(** Bombs-Must-Detonate: Linker 
    @author Brian Go *)

open Linkst

(** List of provided function names *)
type provide_environment = string list

(** Association list of function name - function types *)
type type_environment = (string * link_type) list

exception Linker_error of string

(** Shorthand for String.concat "" *)
let concat s = String.concat "" s

(** Raises the linker error of string s *)
let raise_linker_error s = raise (Linker_error s)

(** VM-Provided Remotable Functions *)
let vm_defs = [ LnkProvide ("to_string",
			    LnkFunction ([LnkAnything],LnkString));
		LnkProvide ("parseint",
			    LnkFunction ([LnkString],LnkInt));
		LnkProvide ("parsefloat",
			    LnkFunction ([LnkString],LnkFloat));
		LnkProvide ("array_length",
			    LnkFunction ([LnkArray LnkAnything],LnkInt));
		LnkProvide ("string_length",
			    LnkFunction ([LnkString],LnkInt));
		LnkProvide ("rand",
			    LnkFunction ([LnkVoid],LnkFloat));
		LnkProvide ("print",
			    LnkFunction ([LnkAnything],LnkVoid));
		LnkProvide ("print_error",
			    LnkFunction ([LnkAnything],LnkVoid));
		LnkProvide ("println",
			    LnkFunction ([LnkAnything],LnkVoid));]

(** Extracts the assembler code from the linker syntax tree *)
let rec extract_code lst = 
  match lst with
      car::cdr -> 
	(match car with 
	     LnkCode s -> s
	   | _ -> extract_code cdr)
    | [] -> ""

(** Extracts a list of the name of a provided function from the linker syntax tree element. Nil otherwise *)
let extract_provide def = match def with
    LnkProvide (name,_) -> [name]
  | _ -> []

(** Extracts the name/type pair of a provided function from the linker syntax tree element. Nil otherwise *)
let extract_provide_type def = match def with
    LnkProvide (name,ty) -> [(name,ty)]
  | _ -> []

(** Extracts a name/type pairs from a linker syntax tree element. Nil otherwisee *)
let extract_type def = match def with
    LnkProvide (name,ty) -> [name, ty]
  | LnkRequire (name,ty) -> [name, ty]
  | _ -> []

(** Checks for duplicate function definitions.  Linker error if found. *)
let rec check_duplicates provide_list = match provide_list with
    car::cdr when List.mem car cdr -> 
      raise_linker_error (concat ["Function ";car;" is provided by more than one actor/vm."])
  | _::cdr -> check_duplicates cdr
  | [] -> ()

(** Check required function against provided functions (just names, not types.  Linker error if not fouond *)
let check_require provide_list def = match def with
    LnkRequire (name,_) ->
      if List.mem name provide_list then
	()
      else
	raise_linker_error (concat ["Function ";name;" was required but not provided."])
  | _ -> ()

(** Checks if two association lists are compatible (do not contain conflicting key/value pairs *)
let rec are_assoclists_compatible l1 l2 = match l1 with
    (x,y)::rest -> 
      if List.mem_assoc x l2 
      then y = List.assoc x l2
      else are_assoclists_compatible rest l2
  | [] -> true


(** Returns an association list of template types to link_types.  Error if types are not unifiable. *)
let rec check_unified t1 t2 = match (t1,t2) with
    x,y when x = y -> []
  | LnkAnything, x -> []
  | x, LnkAnything -> []
  | LnkTemplate s, t2 -> [(s,t2)]
  | t1, LnkTemplate s -> [(s,t1)] (** We do this in either order for convenience: a required function will
				      never be a templated function, since we don't infer that some type
				      is templated; rather we require that the specific types in each case
				      where it is called are valid. *)
  | LnkEnumType (name1,size1), LnkEnumType (name2,size2) ->
      if (name1 = name2) && (size1 = size2) then
	[]
      else 
	raise_linker_error 
	  (concat ["Failed to reconcile enums ";
		   name1;"(";(string_of_int size1);") and ";
		   name2;"(";(string_of_int size2);")."])
  | LnkEnum (name1,size1), LnkEnum (name2,size2) ->
      if (name1 = name2) && (size1 = size2) then
	[]
      else 
	raise_linker_error 
	  (concat ["Failed to reconcile enums ";
		   name1;"(";(string_of_int size1);") and ";
		   name2;"(";(string_of_int size2);")."])
  | LnkStruct (name1,fields1), LnkStruct (name2,fields2) ->
      let _ = List.map2 check_unified fields1 fields2 in
	if name1 = name2 then
	  []
	else 
	  raise_linker_error
	    (concat ["Failed to reconcile structs ";
		     name1;" and ";name2;"."])
  | LnkArray t_arr1, LnkArray t_arr2 ->
      check_unified t_arr1 t_arr2
  | LnkList t_lst1, LnkList t_lst2 ->
      check_unified t_lst1 t_lst2
  | LnkFunction (args1,ret1), LnkFunction (args2,ret2) ->
      let assoc1 = check_unified ret1 ret2 in
      let assoc2 = List.flatten (List.map2 check_unified args1 args2) in
	if are_assoclists_compatible assoc1 assoc2 then
	  assoc1 @ assoc2
	else
	  raise_linker_error "Could not reconcile templated arrow types."
  | LnkRef t_ref1, LnkRef t_ref2 ->
      check_unified t_ref1 t_ref2
  | _ -> raise_linker_error "Irreconciliable types."
	
(** Checks that the name/type list types is consistent with the name/type list of provided_types *)
let rec check_types types provided_types = match types with
    (name,ty)::rest ->
      if List.mem_assoc name provided_types then
	let other_type = List.assoc name provided_types in
	let _ = 
	  try
	    check_unified ty other_type
	  with
	      Linker_error s -> raise_linker_error (concat ["Conflicting type definitions for ";name;": ";s])
	in
	  check_types rest provided_types
      else
	check_types rest provided_types
  | [] -> ()
	
(** Checks that all provide/require definitions in the linker syntax tree are consistent with each other *)
let check_consistent defs = 
  let provided = List.flatten (List.map extract_provide defs) in
  let types = List.flatten (List.map extract_type defs) in
  let provided_types = List.flatten (List.map extract_provide_type defs) in
  let _ = check_duplicates provided in
  let _ = List.map (check_require provided) defs in
  let _ = check_types types provided_types in
    ()

(** Usage:
    "bmdl [src-file-1] [output-file-1] [src-file-2] [output-file-2]"
       Links src-file-1 and src-file-2 and puts the output (linker header removed) in the output files.
       We allow up to 2 source files to be specified because this is the max # of AIs on a team.
    "bmdl [src-file] [output-file]"
       Links a single source file (to the VM definitions) and puts the output in output-file
*)
    
let main () =
  try
    let _ = print_string "Linking...\n"; flush stdout; in
    let n_files = 
      if Array.length Sys.argv >= 5 then 2 else 1
    in
    let filename_list = 
      if n_files = 2 then
	[Sys.argv.(1); Sys.argv.(3)]
      else 
	[Sys.argv.(1)]
    in
    let outfile_names = 
      if n_files = 2 then
	[Sys.argv.(2); Sys.argv.(4)]
      else
	[Sys.argv.(2)]
    in
    let file_list = List.map open_in filename_list in
    let lexing_list = List.map Lexing.from_channel file_list in
      (** Parse files *)
    let linkst_list = List.map (Bmdlinkparse.link Bmdlinklex.bmd_link) lexing_list in
      (** Extract code *)
    let code_list = List.map extract_code linkst_list in
      (** Check for consistency *)
    let defs = List.flatten (vm_defs::linkst_list) in
    let _ = check_consistent defs in
      (** Output code *)
    let outfile_list = List.map open_out outfile_names in
    let _ = List.map2 output_string outfile_list code_list in
    let _ = print_string "Done.\n"; flush stdout; in
      ()
  with
      Parsing.Parse_error ->
	begin
	  print_string "Syntax error on line ";
	  print_string (string_of_int !Bmdlinklex.line_count);
	  print_string ".\n";
	  flush stdout;
	end
    | Sys_error s ->
	begin
	  print_string "Could not open input file ";
	  print_string s;
	  print_string "\n";
	  flush stdout;
	end
    | Linker_error s ->
	begin
	  print_string s;
	  print_string "\n";
	  flush stdout;
	end
    | _ ->
	begin
	  print_string "Linking failed.\n";
	end

let _ = main ()
